﻿using System;
using System.Collections.Generic;
using System.Linq;

using mvcForum.Core;
using mvcForum.Core.Abstractions.Interfaces;
using mvcForum.Core.Events;
using mvcForum.Core.Interfaces.Events;

using mvcForum.Web.Extensions;
using mvcForum.Core.Specifications;
using mvcForum.Core.Interfaces.Services;
using mvcForum.Core.Interfaces.Data;

namespace mvcForum.Web.Events {

	public class NewAndUpdatedContentEventListener : IAsyncEventListener<NewPostEvent>,
														IAsyncEventListener<NewTopicEvent>,
														IAsyncEventListener<PostUpdatedEvent>,
														IAsyncEventListener<TopicUpdatedEvent>,
														IAsyncEventListener<TopicMergedEvent>,
														IAsyncEventListener<TopicSplitEvent>,
														IAsyncEventListener<TopicMovedEvent> {
		private readonly ILogger logger;
		private readonly IAsyncTask task;
		private readonly IConfiguration config;
		private readonly IContext context;

		public NewAndUpdatedContentEventListener(ILogger logger, IContext context, IAsyncTask task, IConfiguration config) {
			this.logger = logger;
			this.task = task;
			this.config = config;
			this.context = context;
		}

		#region Queue methods
		/// <summary>
		/// Method for queueing the updated post event for later/asynchronously handling
		/// </summary>
		/// <param name="payload"></param>
		public void Queue(PostUpdatedEvent payload) {
			this.logger.Log(EventType.Debug, String.Format("The updated post {0} is queued.", payload.PostId));
			// TODO: Delay!!
			this.task.Execute(this, payload, 3);
		}

		/// <summary>
		/// Method for queueing the updated topic event for later/asynchronously handling
		/// </summary>
		/// <param name="payload"></param>
		public void Queue(TopicUpdatedEvent payload) {
			this.logger.Log(EventType.Debug, String.Format("The updated topic {0} is queued.", payload.TopicId));
			// TODO: Delay!!
			this.task.Execute(this, payload, 3);
		}

		/// <summary>
		/// Method for queueing the split topic event for later/asynchronously handling
		/// </summary>
		/// <param name="payload"></param>
		public void Queue(TopicSplitEvent payload) {
			this.logger.Log(EventType.Debug, String.Format("The split topic {0} is queued.", payload.OriginalTopicId));
			// TODO: Delay!!
			this.task.Execute(this, payload, 3);
		}

		/// <summary>
		/// Method for queueing the new topic event for later/asynchronously handling
		/// </summary>
		/// <param name="payload"></param>
		public void Queue(NewTopicEvent payload) {
			this.logger.Log(EventType.Debug, String.Format("The new topic {0} is queued.", payload.TopicId));
			// TODO: Delay!!
			this.task.Execute(this, payload, 3);
		}

		/// <summary>
		/// Method for queueing the new post event for later/asynchronously handling
		/// </summary>
		/// <param name="payload"></param>
		public void Queue(NewPostEvent payload) {
			this.logger.Log(EventType.Debug, String.Format("The new post {0} is queued.", payload.PostId));
			// TODO: Delay!!
			this.task.Execute(this, payload, 3);
		}

		public void Queue(TopicMergedEvent payload) {
			this.logger.Log(EventType.Debug, String.Format("The merged topic {0} is queued.", payload.TopicId));
			// TODO: Delay!!
			this.task.Execute(this, payload, 3);
		}

		public void Queue(TopicMovedEvent payload) {
			this.logger.Log(EventType.Debug, String.Format("The moved topic {0} is queued.", payload.TopicId));
			// TODO: Delay!!
			this.task.Execute(this, payload, 3);
		}
		#endregion

		#region Handle methods
		public void Handle(Object payload) {
			if (payload is TopicUpdatedEvent) {
				this.Handle((TopicUpdatedEvent)payload);
			}
			else if (payload is PostUpdatedEvent) {
				this.Handle((PostUpdatedEvent)payload);
			}
			else if (payload is NewTopicEvent) {
				this.Handle((NewTopicEvent)payload);
			}
			else if (payload is NewPostEvent) {
				this.Handle((NewPostEvent)payload);
			}
			else if (payload is TopicMergedEvent) {
				this.Handle((TopicMergedEvent)payload);
			}
			else if (payload is TopicMovedEvent) {
				this.Handle((TopicMovedEvent)payload);
			}
			else if (payload is TopicSplitEvent) {
				this.Handle((TopicSplitEvent)payload);
			}
			else {
				throw new ApplicationException("Unknown payload!");
			}
		}

		public void Handle(TopicMergedEvent payload) {
			// Let's get the new topic.
			Topic topic = this.context.GetRepository<Topic>().Read(payload.TopicId);
			// Log info!
			this.logger.Log(EventType.Debug, String.Format("A merged topic '{1}' ({0}).", topic.Id, topic.Title));

			IEnumerable<Post> posts = topic.Posts.Visible(this.config);
			topic.PostCount = posts.Count() - 1;
			if (posts.Any()) {
				Post latestPost = posts.OrderByDescending(p => p.Posted).First();
				topic.LastPostId = latestPost.Id;
				topic.LastPostAuthorId = latestPost.Author.Id;
				topic.LastPosted = latestPost.Posted;
				topic.LastPostUsername = latestPost.AuthorName;
			}

			this.context.SaveChanges();

			UpdateForum(topic.Forum, this.context);
		}

		public void Handle(TopicMovedEvent payload) {
			// Let's get the source forum.
			Forum sourceForum = this.context.GetRepository<Forum>().Read(payload.SourceForumId);
			// Let's get the source forum.
			Forum destinationForum = this.context.GetRepository<Forum>().Read(payload.DestinationForumId);
			// Log info!
			this.logger.Log(EventType.Debug, String.Format("A topic was moved ({0}).", payload.TopicId));

			UpdateForum(sourceForum, this.context);

			UpdateForum(destinationForum, this.context);
		}

		/// <summary>
		/// Method for handling the updated post event.
		/// </summary>
		/// <param name="payload">Any relevant info regarding the updated post.</param>
		public void Handle(PostUpdatedEvent payload) {
			// TODO: Do anything on post update? Anything to do with the status/flag is handled elsewhere!
		}

		/// <summary>
		/// Method for handling the updated topic event.
		/// </summary>
		/// <param name="payload">Any relevant info regarding the updated topic.</param>
		public void Handle(TopicUpdatedEvent payload) {
			// TODO: Do anything on topic update? Anything to do with the status/flag is handled elsewhere!
		}

		/// <summary>
		/// Method for handling the new topic event.
		/// </summary>
		/// <param name="payload">Any relevant info regarding the new topic.</param>
		public void Handle(NewTopicEvent payload) {
			// Let's get the new topic.
			Topic topic = this.context.GetRepository<Topic>().Read(payload.TopicId);
			// Log info!
			this.logger.Log(EventType.Debug, String.Format("A new topic '{1}', was created, time to index it ({0}).", topic.Id, topic.Title));
			// Has the topic been deleted or put in quarantine?
			if (topic.Flag == TopicFlag.None || topic.Flag == TopicFlag.Locked) {
				// Topic is visible, let's update the topic count on the parent forum(s).

				this.context.SaveChanges();

				UpdateForum(topic.Forum, this.context);
			}
		}

		/// <summary>
		/// Method for handling a topic split.
		/// </summary>
		/// <param name="payload">Any relevant info regarding the new topic.</param>
		public void Handle(TopicSplitEvent payload) {
			// Let's get the original topic.
			Topic originalTopic = this.context.GetRepository<Topic>().Read(payload.OriginalTopicId);
			// Let's get the new topic.
			Topic newTopic = this.context.GetRepository<Topic>().Read(payload.NewTopicId);
			// Log info!
			this.logger.Log(EventType.Debug, String.Format("A new topic '{1}', was created by splitting '{0}' in two.", originalTopic.Title, newTopic.Title));

			// Let's update the post count on the topic.
			IEnumerable<Post> posts = newTopic.Posts.Where(p => p.FlagValue == (Int32)PostFlag.None && p.Position != 0);
			newTopic.PostCount = posts.Count();
			// And the latest post info!
			if (newTopic.PostCount > 0) {
				Post latestPost = posts.OrderByDescending(p => p.Posted).First();
				newTopic.LastPost = latestPost;
				newTopic.LastPosted = latestPost.Posted;
				newTopic.LastPostAuthor = latestPost.Author;
				newTopic.LastPostUsername = latestPost.AuthorName;
			}

			// Let's update the post count on the topic.
			posts = originalTopic.Posts.Where(p => p.FlagValue == (Int32)PostFlag.None && p.Position != 0);
			originalTopic.PostCount = posts.Count();
			// And the latest post info!
			if (newTopic.PostCount > 0) {
				Post latestPost = posts.OrderByDescending(p => p.Posted).First();
				originalTopic.LastPost = latestPost;
				originalTopic.LastPosted = latestPost.Posted;
				originalTopic.LastPostAuthor = latestPost.Author;
				originalTopic.LastPostUsername = latestPost.AuthorName;
			}

			this.context.SaveChanges();

			UpdateForum(newTopic.Forum, this.context);
		}

		/// <summary>
		/// Method for handling the new post event.
		/// </summary>
		/// <param name="payload">Any relevant infor regarding the new post.</param>
		public void Handle(NewPostEvent payload) {
			// Let's get the new post!
			Post post = this.context.GetRepository<Post>().Read(payload.PostId);
			// Log info!
			this.logger.Log(EventType.Debug, String.Format("A new post '{1}', was created, time to index it ({0}).", post.Id, post.Subject));
			// Has the post been deleted or put in quarantine?
			if (post.Flag == PostFlag.None) {
				// Post is visible, let's update the post count on the topic.
				post.Topic.PostCount++;
				// And the latest post info!
				post.Topic.LastPost = post;
				post.Topic.LastPosted = post.Posted;
				post.Topic.LastPostAuthor = post.Author;
				post.Topic.LastPostUsername = post.AuthorName;

				this.context.SaveChanges();

				UpdateForum(post.Topic.Forum, this.context);
			}
		}
		#endregion

		/// <summary>
		/// Updates the latest post/topic and the topic/post count on the given forum.
		/// If the forum is a sub forum, this method will be called on the parent forum too.
		/// </summary>
		/// <param name="forum"></param>
		public static void UpdateForum(Core.Forum forum, IContext context) {
			// Get the list of visible topics (not deleted and not in quarantine).
			IEnumerable<Topic> visibleTopics = forum.Topics.AsQueryable().Where(new TopicSpecifications.Visible().IsSatisfied).OrderByDescending(t => t.LastPosted);
			// Get the latest topic.
			Topic latestTopic = visibleTopics.FirstOrDefault();

			// Get the list of latest topics from any sub forums!
			IEnumerable<Topic> subForumTopics = forum.SubForums.Select(f => f.LastTopic).Where(t => t != null).OrderByDescending(t => t.LastPosted);
			// Get the latest of those.
			Topic latestSubTopic = subForumTopics.FirstOrDefault();

			// Get the latest topic (the most recent).
			if (latestTopic != null || latestSubTopic != null) {
				// We do have a latest topic from somewhere!

				// Do we have one from this forum and one from the sub forums?
				if (latestSubTopic != null && latestTopic != null) {
					// Yes, is the one from the sub forums the latest?
					if (latestTopic.LastPosted < latestSubTopic.LastPosted) {
						// Yes, let's store that for later use then!
						latestTopic = latestSubTopic;
					}
				}
				else if (latestSubTopic != null) {
					// No topics in this forum, get the latest from the sub forums.
					latestTopic = latestSubTopic;
				}
			}

			// Did we get a latest topic?
			if (latestTopic == null) {
				// No, let's reset then!
				ResetLatest(forum);
			}
			else {
				// Yes, let's store in on the forum then.
				SetLatest(forum, latestTopic);
			}

			// Add the number of visible topics to the sum of topics from the sub forums
			forum.TopicCount = visibleTopics.Count() + forum.SubForums.Sum(f => f.TopicCount);
			// Add the number of visible posts to the sum of posts from the sub forums
			forum.PostCount = visibleTopics.SelectMany(t => t.Posts).Where(p => p.Position != 0 && p.FlagValue == (Int32)PostFlag.None).Count() +
								forum.SubForums.Sum(f => f.PostCount);

			// Save any changes!
			context.SaveChanges();

			// Do this forum have a parent?
			if (forum.ParentForum != null) {
				// Yes, let's update that too then!
				UpdateForum(forum.ParentForum, context);
			}
		}

		/// <summary>
		/// Sets the given topic as the latest posted topic in the given forum.
		/// </summary>
		/// <param name="forum"></param>
		/// <param name="latestTopic"></param>
		private static void SetLatest(Core.Forum forum, Topic latestTopic) {
			// Set the topic on the forum
			forum.LastTopic = latestTopic;
			// Is this a topic without any posts (yet) ?
			if (latestTopic.LastPost == null) {
				// Yes, so the latest post is the topic itself!
				Post topicPost = latestTopic.Posts.OrderBy(p => p.Posted).First();
				forum.LastPost = topicPost;
				forum.LastPosted = topicPost.Posted;
				forum.LastPostUser = topicPost.Author;
				forum.LastPostUsername = topicPost.AuthorName;
			}
			else {
				// Just a regular topic with replies/posts.
				forum.LastPost = latestTopic.LastPost;
				forum.LastPosted = latestTopic.LastPosted;
				forum.LastPostUser = latestTopic.LastPostAuthor;
				forum.LastPostUsername = latestTopic.LastPostUsername;
			}
		}

		/// <summary>
		/// Reset the latest topic/post of the given forum.
		/// </summary>
		/// <param name="forum"></param>
		private static void ResetLatest(Core.Forum forum) {
			// For some reason there are no longer any topics/posts in the forum, reset it!
			forum.LastTopic = null;
			forum.LastPost = null;
			forum.LastPosted = null;
			forum.LastPostUser = null;
			forum.LastPostUsername = String.Empty;
		}

		/// <summary>
		/// True if this handler can run asynchronously.
		/// </summary>
		public Boolean RunAsynchronously {
			get {
				// TODO: Get from config!!
				return false;
			}
		}

		/// <summary>
		/// The priority of this handler.
		/// </summary>
		public Byte Priority {
			get {
				return (Byte)EventPriority.Lowest;
			}
		}

		public Boolean UniqueEvent {
			get {
				return false;
			}
		}
	}
}